Stable Captives — DOM/PAHE Developer Integration Guide

## 1. Purpose and ownership boundary

Stable Captives fixes the naked spawned-bandit problem by cloning the resolved ActorBase of a captured spawned actor into one of 1,024 persistent template ActorBases defined by `NoeActorClone.esp`.

The original shared or leveled ActorBase is never modified. The managed clone is forced to be Unique, while Respawn and template inheritance are disabled. All other source ActorBase values are preserved unless the owner mod explicitly uses API 6 to change them.

The public mod and MCM name is **Stable Captives**. Technical filenames, Papyrus script names and API names intentionally remain `NoeActorClone`.

The DLL owns only:

- ActorBase template allocation and persistence
- FaceGen copying and NIF FaceTint-path patching
- managed-clone identity and slot ownership
- optional ActorBase/outfit configuration requested by the owner mod
- visual scale persistence
- safe retirement after confirmed permanent Actor deletion

The DLL does **not** own or modify:

- DOM/PAHE/HSH/AYGAS factions
- dialogue or commands
- quest aliases
- AI packages
- relationships, training, mood or slave state
- transfer, sale or residence logic
- live Actor-reference inventory decisions

DOM and PAHE should initiate cloning from their own stable capture/clone path. No transient capture-faction polling or background DOM interface is required.

## 2. Runtime contract

`NoeActorClone.GetAPIVersion()` currently returns `6`.

The normal transaction is:

1. The owner mod calls `CloneBaseWithFlags()`.
2. The DLL performs a circular search for a free template.
3. The template is globally leased and marked `Reserved` before FaceGen or ActorBase data is copied.
4. The DLL returns the reserved ActorBase, or `None` after a complete 1,024-slot search or any preparation failure.
5. The owner may optionally call `ConfigureActorBase()` while the ActorBase is still reserved.
6. The owner creates the Actor reference with `PlaceActorAtMe()`.
7. The owner calls `CommitClone()` immediately after successful creation.
8. After owner-side copying and initialization are complete, the owner calls `FinalizeManagedClone()`.
9. Any failure before commit must call `CancelCloneBase()`.

Minimal structure:

```papyrus
ActorBase targetBase = NoeActorClone.CloneBaseWithFlags(sourceActor, cloneFlags)

If targetBase != None
    ; Optional API 6 configuration belongs here.

    Actor clone = spawnMarker.PlaceActorAtMe(targetBase, 4)
    If clone != None
        If NoeActorClone.CommitClone(clone)
            ; Perform the normal DOM/PAHE Actor-reference work.
            ; Call FinalizeManagedClone(clone) when that work is complete.
            Return clone
        EndIf

        clone.Disable()
        clone.Delete()
    EndIf

    NoeActorClone.CancelCloneBase(targetBase)
EndIf

; Use the existing DOM/PAHE fallback.
Return None
```

The included integration points are currently:

- `DOM_Core.psc`, `Actor Function CloneActor(Actor original)`
  - `CloneBaseWithFlags()` around line 9224
  - Actor creation around line 9229
  - `CommitClone()` around line 9231
- `pahcore.psc`, `Actor Function Clone(Actor original)`
  - `CloneBaseWithFlags()` around line 1541
  - Actor creation around line 1546
  - `CommitClone()` around line 1548

Line numbers may move as DOM/PAHE changes. The invariant is more important: optional ActorBase configuration must occur after reservation and before Actor creation.

If `IsManagedClone(original)` is already true, return or reuse that Actor instead of consuming another slot. This is why one NPC continues to use one ActorBase while moving between DOM, PAHE, HSH and AYGAS.

## 3. Inventory and outfit flags — API 5

```papyrus
ActorBase targetBase = NoeActorClone.CloneBaseWithFlags(sourceActor, cloneFlags)
```

`cloneFlags` is a bit mask:

- `0`: copy neither live inventory nor ActorBase outfits
- `1`: allow the owner mod to copy the live Actor-reference inventory
- `2`: copy ActorBase default and sleep outfits in the DLL
- `3`: both

Inventory is Actor-reference data. The DLL cannot and should not replace DOM's inventory-transfer logic. DOM should run its existing inventory-copy operation only when bit `1` is set:

```papyrus
Bool copyInventory = NoeActorClone.HasCloneFlag(clone, 1)
If copyInventory
    ; Run DOM's existing live inventory copy.
EndIf
```

Bit `2` controls whether Stable Captives retains the source ActorBase default and sleep outfits. Leaving bit `2` clear prevents the cloned ActorBase from periodically reapplying the original spawned outfit. This is separate from clothing or restraints later equipped on the live Actor by DOM, HSH or AYGAS.

The included test integration uses `cloneFlags = 0`. DOM may expose values `0–3` through its own MCM if desired. Stable Captives deliberately does not define DOM's user-facing outfit policy.

Call `FinalizeManagedClone(clone)` after DOM/PAHE finishes its normal clone initialization. It performs the final outfit detachment or retention according to bit `2` and queues the visual refresh. It does not touch factions, aliases, dialogue or AI.

## 4. Optional ActorBase flags and leveling — API 6

API 6 was added in response to the request for editable ActorBase flags or follower-style defaults.

```papyrus
Bool Function ConfigureActorBase(
    ActorBase akBase,
    Int aiFlagsToSet = 0,
    Int aiFlagsToClear = 0,
    Float afPCLevelMult = -1.0,
    Int aiFixedLevel = -1,
    Int aiMinLevel = -1,
    Int aiMaxLevel = -1
) Global Native
```

This function is optional. If it is not called, Stable Captives preserves the resolved source ActorBase's flags and leveling values, except for the mandatory identity rules:

- Unique is enabled
- Respawn is disabled
- UsesTemplate/template inheritance is disabled

Call `ConfigureActorBase()` only after `CloneBase()` or `CloneBaseWithFlags()` returns a reserved ActorBase and before `PlaceActorAtMe()` creates the Actor reference.

### Level arguments

- `afPCLevelMult = -1.0`: preserve the source setting
- `afPCLevelMult = 0.001..65.535`: enable PC Level Mult and use that multiplier
- `aiFixedLevel = -1`: preserve the source setting
- `aiFixedLevel = 1..65535`: disable PC Level Mult and use a fixed level
- `aiMinLevel` and `aiMaxLevel = -1`: preserve each source value
- explicit minimum/maximum values may be `0..65535`
- multiplier and fixed level cannot be supplied in the same call

PC Level Mult is based on the **player's level**, not the source actor's current level. For example, a multiplier of `4.0` produces level 40 when the player is level 10, subject to the supplied minimum and maximum.

If DOM wants “captured actor's current level multiplied by N,” it should calculate a fixed level before Actor creation:

```papyrus
Int desiredLevel = sourceActor.GetLevel() * 2
Bool configured = NoeActorClone.ConfigureActorBase(
    targetBase,
    0,
    128,
    -1.0,
    desiredLevel,
    -1,
    -1
)
```

### Examples

Preserve the source completely:

```papyrus
; Do not call ConfigureActorBase().
```

Player-level multiplier 1.0, range 10–100:

```papyrus
Bool configured = NoeActorClone.ConfigureActorBase(
    targetBase,
    128,
    0,
    1.0,
    -1,
    10,
    100
)
```

Fixed level 25:

```papyrus
Bool configured = NoeActorClone.ConfigureActorBase(
    targetBase,
    0,
    128,
    -1.0,
    25,
    -1,
    -1
)
```

If configuration returns `false`, call `CancelCloneBase(targetBase)` and use DOM's existing fallback. Do not create an Actor from a failed reservation.

### Editable flag bits

- `2` Essential
- `16` Auto Calc Stats
- `64` Doesn't Affect Stealth
- `128` PC Level Mult
- `2048` Protected
- `16384` Summonable
- `65536` Doesn't Bleed
- `262144` Bleedout Override
- `524288` Opposite Gender Animations
- `1048576` Simple Actor
- `2097152` Looped Script
- `268435456` Looped Audio
- `536870912` Ghost
- `-2147483648` Invulnerable, the signed Papyrus form of `0x80000000`

The set and clear masks may not overlap. Enabling bit `128` requires an explicit multiplier; clearing bit `128` requires an explicit fixed level.

### Recommended DOM policy

Stable Captives should continue to preserve source values by default. DOM may choose one of the following designs:

- hard-code DOM's preferred follower-style defaults at the two clone call sites; or
- expose a DOM MCM level policy and translate it into API 6 arguments.

Suggested choices are:

- Preserve source values
- PC Level Multiplier with minimum and maximum
- Fixed level
- DOM follower preset

Essential and Protected should preferably remain under DOM's existing policy rather than being forced by Stable Captives. The exact follower preset and MCM design are intentionally left to the DOM developer.

## 5. Slot allocation

The pool contains 1,024 ActorBase templates. Allocation uses a persisted circular search origin rather than always scanning from slot zero.

Lifecycle states are:

- `Free`
- `Reserved`
- `Active`
- `Retiring`

A slot is globally leased before FaceGen or ActorBase copying starts. If a full circular pass returns to its starting point without finding a free template, `CloneBaseWithFlags()` returns `None` and the owner mod should use its existing fallback.

Leases are shared across save flows so that a second save does not overwrite a slot still used by another save. Moving the same Actor among DOM, PAHE, HSH and AYGAS does not consume another slot.

## 6. Release and permanent deletion

`ReleaseActorBase(actor)` must be called only when the owner mod is about to permanently delete that managed Actor reference.

```papyrus
Bool retiring = NoeActorClone.ReleaseActorBase(actor)
```

The call changes the slot from `Active` to `Retiring`; it does not immediately mark it `Free`. The reference may still be used by DOM scripts, aliases or Skyrim's asynchronous FaceGen/IO workers while `DeleteWhenAble()` is pending.

The safe DOM sequence is:

1. Decide that this managed spawned clone will be permanently deleted.
2. Call `ReleaseActorBase(actor)`.
3. If it returns `false`, keep the Actor and do not continue deletion.
4. Release the DOM alias while the DOM ownership factions are still present, so the correct manager can be found.
5. Remove factions as required by DOM.
6. Call `Disable()` and `DeleteWhenAble()`.
7. Stable Captives keeps the slot `Retiring` until the Actor reference no longer resolves after load-time checks.
8. Only then is the global lease returned and the slot changed to `Free`.

The existing integration is in `DOM_Actor.psc`, `ReleaseOrDelete()`, around the managed-clone branch near lines 1268–1287.

Do **not** call `ReleaseActorBase()` for:

- transfer between DOM, PAHE, HSH or AYGAS
- sale or residence placement while the Actor continues to exist
- ordinary release where the freed NPC remains in the world
- temporary alias removal
- disable without permanent deletion

If the released Actor remains in the world, its slot must remain `Active`. Reusing that ActorBase would change the appearance or base data of the living released NPC.

### Optional DOM release policy

DOM's current `ReleaseOrDelete()` behavior may keep a released Actor in the world or permanently remove it depending on mood, transfer state and submission threshold. This is valid, but it means only the permanent-deletion outcome can retire and eventually free the Stable Captives slot.

If desired, DOM could add an owner-controlled release policy for managed spawned clones. A possible MCM design is:

- `Existing DOM behavior` — preserve the present conditional logic
- `Always delete managed spawned clones on release` — use the safe retirement and permanent-deletion sequence above

This option belongs in DOM because it decides whether a released NPC remains in the game world. Stable Captives must not make that gameplay decision autonomously.

The second option should not mark the slot `Free` directly. It must call `ReleaseActorBase()` first and allow the `Retiring` safeguard to confirm deletion.

Whether this option should be added, and how it should be presented to users, is entirely the DOM developer's decision.

## 7. FaceGen requirements

The clone's source FaceGen may come from loose files or another mod's BSA. The DLL reads source resources through Skyrim's resource system, copies the NIF and DDS, patches the NIF's internal FaceTint path to the allocated slot, and writes the final slot files atomically.

Skyrim does not reliably discover a completely new FaceGen destination path during the same game session. Therefore the distribution contains:

- 1,024 loose zero-byte NIF path placeholders
- 1,024 loose zero-byte DDS path placeholders

These exact paths must exist before Skyrim starts. At capture time the allocated pair is replaced with real FaceGen data before the Actor reference is created.

Do not remove these files and do not place them in a BSA. A BSA experiment demonstrated that Skyrim retained the BSA archive provider even after valid nonzero loose output had been created. The FaceGen worker then attempted to parse the zero-byte archive DDS and crashed. The loose placeholders are therefore a runtime requirement, not packaging clutter.

When a slot becomes `Free`, its final loose NIF/DDS is not truncated back to zero. The next allocation atomically replaces it. Factory Reset clears the shared slot registry but does not restore final FaceGen files to zero-byte placeholders.

## 8. MCM maintenance semantics

`Reclaim confirmed deleted Actors` examines only `Retiring` records. It never forcibly reclaims an `Active` Actor or an Actor reference that still resolves.

`Factory Reset`:

- is refused if the current save owns any Active, Reserved or Retiring slot
- clears the shared global lease registry from an appropriate clean/pre-capture save
- defers orphan internal FaceGen-cache cleanup until a cold game start
- does not edit DOM/PAHE/HSH/AYGAS records
- does not delete or zero the final slot NIF/DDS files

Restoring every packaged placeholder to zero bytes requires a full game exit and reinstalling the clean placeholder files. It should not be attempted from an in-game MCM operation.

## 9. Failure and fallback requirements

The owner mod should retain its old clone path for all of the following cases:

- Stable Captives is not installed
- unsupported API version
- no free slot after a complete circular scan
- FaceGen source cannot be read
- ActorBase copy fails
- API 6 configuration fails
- Actor creation fails
- `CommitClone()` fails

Every uncommitted reservation must be cancelled. Every committed Actor that is intentionally and permanently removed must use the retirement path.

## 10. Summary for DOM

- Let DOM/PAHE choose when cloning occurs.
- Call `CloneBaseWithFlags()` from the stable owner clone path.
- Optionally call `ConfigureActorBase()` before Actor creation.
- Let DOM create and initialize the Actor reference.
- Call `CommitClone()` and later `FinalizeManagedClone()`.
- Keep all faction, dialogue, alias, AI and slave-state logic in DOM/PAHE.
- Keep one slot while the Actor exists anywhere in DOM/PAHE/HSH/AYGAS.
- Call `ReleaseActorBase()` only immediately before confirmed permanent deletion.
- Consider, at DOM's discretion, an MCM policy for managed spawned clones that remain in the world after release versus permanent deletion.
- Consider, at DOM's discretion, a level/ActorBase preset using API 6; source preservation remains the safest default.
